/*
MiscSerialPort.m -- an OO wrapper around the serial ports
Written by Matt Brandt Copyright ( c ) 1994 by Matt Brandt.
Modified by Arnaud Meuret
Version 2.0.  All rights reserved.
This notice may not be removed from this source code.

	This object is included in the MiscKit by permission from the author
	and its use is governed by the MiscKit license, found in the file
	"LICENSE.rtf" in the MiscKit distribution.  Please refer to that file
	for a list of all applicable permissions and restrictions.
______________________________________________________________________________*/

#import "MiscSerialPort.h"

struct
{
	int		intValue;
	int		name;
} baudRateTable[] = {
	{110,B110},
	{300,B300},
	{600,B600},
	{1200,B1200},
	{2400,B2400},
	{4800,B4800},
	{9600,B9600},
	{14400,B14400},
	{19200,B19200},
	{28800,B28800},
	{38400,B38400},
	{43200,B43200},
	{57600,B57600},
	{"",0}};

int					origLineDisc;
struct sgttyb		origTtyParms;
struct tchars		origSpecChars;
struct ltchars		origLocSpecChars;
int					origLocMode;

@implementation MiscSerialPort

//========================================================================
- init
{
	ttyParms.sg_flags = 0;					// clear all delay selection
	ioctl( portFD,TIOCEXCL,&ttyParms );		// exclusive access
	ioctl( portFD,TIOCHPCL,&ttyParms );		// hang up on close
	
	// These set the default behaviour of the instances
	[self setDeviceName:"/dev/cufa"];		// port A (??name?? on white) with hard flow control
	flags.connected = NO;					// not connected
	flags.suspended = NO;					// not suspended
	delegate = nil;							// no default delegate
	[self setBaud:B9600];					// 9600 bauds
	[self setParity:MISC_SP_NONE];			// no parity
	[self setMode:MISC_SP_COOKED];			// full I/O processing
	[self translateNL:YES];					// translate NewLines
	[self setEchoing:NO];					// do not echo received chars
	[self setFlowControl:MISC_SP_HARDFC];	// assume hardware flow control
	return self;
}

//========================================================================
- free
{
	[self disconnect];
	return [super free];		// so long and thanks for all the fish.
}

//========================================================================
- ( BOOL )connect
{
	int ldisc = NTTYDISC;

	if( flags.connected )			// do nothing if already connected
		return YES;

	if( ![self deviceIsAvailable:device])
	{
		status = MISC_SP_CANT_LOCK;
		return NO;
	}
		
	portFD = open( device,O_RDWR );
	if( portFD < 0 )
		return NO;

	// Save current parameters
	ioctl( portFD, TIOCGETD, &origLineDisc );
	ioctl( portFD, TIOCGETP, &origTtyParms );
	ioctl( portFD, TIOCGETC, &origSpecChars );
	ioctl( portFD, TIOCLGET, &origLocMode );
	ioctl( portFD, TIOCGLTC, &origLocSpecChars );

	ttyParms = origTtyParms;
					
	// set new discipline
	ioctl( portFD, TIOCSETD, &ldisc );
	// pass our parameters structure to the driver ( I/O buffers are flushed )
	ioctl( portFD, TIOCSETD, &ttyParms );
	
	flags.connected = YES;
		
	// setup file descriptor monitor, the function we register willl be called when something
	// pops up on the port.
	DPSAddFD( portFD, ( DPSFDProc )portEventHandler, self, NX_MODALRESPTHRESHOLD );
	flags.suspended = NO;
	
	status = MISC_SP_OK;
	return YES;
}

//========================================================================
- disconnect
{
	if( !flags.connected )
		return self;

	if( !flags.suspended )
		DPSRemoveFD( portFD );

	// Be a good object and put everything back as it was when we took it!
	ioctl( portFD, TIOCSETD, &origLineDisc );
	ioctl( portFD, TIOCSETP, &origTtyParms );
	ioctl( portFD, TIOCSETC, &origSpecChars );
	ioctl( portFD, TIOCLSET, &origLocMode );
	ioctl( portFD, TIOCSLTC, &origLocSpecChars );

	// O.K. I dont bother setting back the "exclusive-use" bit if it has
	// changed, but does it really matter? Anyway, the "hang-up-on-close" bit
	// does not seem to be reset-able !

	if(close( portFD )<0)
	{
		status = MISC_SP_SEE_ERRNO;
		return self;
	}
	flags.connected = NO;
	status = MISC_SP_OK;
	return self;
}

//========================================================================
// The convention is to have the accessor and modifier methods use
// the ivar name, however setDevice is a method declared in NXSoundStream!
- setDeviceName: ( const char * )name
{
	if( strcmp( name,device ) == 0 )
		return self;
		
	strncpy( device,name,DEVICE_PATH_LEN );
	device[DEVICE_PATH_LEN+1] = '\0';

	if( ![self deviceIsAvailable:name])
	{
		status = (MISC_SP_CANT_LOCK & MISC_SP_NO_DEVICE & MISC_SP_NOT_OPEN);
		return NO;
	}

	if( flags.connected  )
	{
		[self disconnect];
		[self connect];
	}
	return self;
}

//========================================================================
- ( BOOL ) deviceIsAvailable: ( const char * )name
{
	id		lockFilename;
	
	lockFilename = [[MiscString alloc] initString:"/usr/spool/uucp/LCK/LCK.."];
	
	// concat the name of the device to the lockfile name
	[lockFilename concatenate:[[lockFilename filename] stringValueAndFree]];

	miscLock = [[MiscLockFile alloc] init];
	[miscLock setFileName:lockFilename];
	
	if ([miscLock lock] )
	{
		[miscLock unlock];
		return YES;
	}
	else
		return NO;
}

//========================================================================
- setFlowControl:( int )method;
{
	switch( method )
	{
	case MISC_SP_NOFC:
		ttyParms.sg_flags &= ~TANDEM;		// clear XON/XOFF Flow control bit
		flags.hardwareFC = NO;
		break;

	case MISC_SP_HARDFC:
		ttyParms.sg_flags &= ~TANDEM;		// clear XON/XOFF Flow control bit
		flags.hardwareFC = YES;
		break;

	case MISC_SP_SOFTFC:
		ttyParms.sg_flags |= TANDEM;		// set XON/XOFF Flow control bit
		flags.hardwareFC = NO;
		break;
		
	default:
		ttyParms.sg_flags &= ~TANDEM;		// clear XON/XOFF Flow control bit
		flags.hardwareFC = YES;
		break;
	}
		
	if( flags.connected  )
		ioctl( portFD,TIOCSETP,&ttyParms );

	status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;
	return self;
}

//========================================================================
- (int) flowControl;
{
	if( flags.hardwareFC )
		return MISC_SP_HARDFC;
		
	if( ttyParms.sg_flags & TANDEM )
		return MISC_SP_SOFTFC;
	else
		return MISC_SP_NOFC;
}

//========================================================================
- dropDTR
{
	if( flags.connected )
		ioctl( portFD, TIOCCDTR,0 );
	status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;
	return self;
}

//========================================================================
- raiseDTR
{
	if( flags.connected )
		ioctl( portFD, TIOCSDTR,0 );
	status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;
	return self;
}

//========================================================================
- setBaudRateByName: ( int )name
{		
	ttyParms.sg_ispeed = name;
	ttyParms.sg_ospeed = name;
	if( flags.connected )
		ioctl( portFD,TIOCSETP,&ttyParms );
	status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;
	return self;
}

//========================================================================
- setBaudRate: ( int )speed
{
UBYTE		i=0;

	// we try to find the first standard speed at least equal to what is requested
	speed = ( int ) ceil( speed );
	
	while( baudRateTable[i].name )
	{
		if( baudRateTable[i].intValue >=speed )
			return [self setBaud: baudRateTable[i].name];
		i++;
	}
	[self setBaud: B19200];
	return self;
}

//========================================================================
- setBaudRateFromString: ( const char* )speed
{
	// I thought of doing an sprintf on speed to check for a numerical value but it would probably
	// be useful in 1 case over 42:-).
	return [self setBaudRate:atoi( speed )];
}

//========================================================================
- setParity: ( int )parity
{
	switch( parity )
	{
	case MISC_SP_NONE:
		ttyParms.sg_flags &= ( ~EVENP | ~ODDP );
		break;

	case MISC_SP_ODD:
		ttyParms.sg_flags |= ODDP;
		ttyParms.sg_flags &= ~EVENP;
		break;

	case MISC_SP_EVEN:
		ttyParms.sg_flags |= EVENP;
		ttyParms.sg_flags &= ~ODDP;
		break;
		
	default:
		ttyParms.sg_flags &= ( ~EVENP | ~ODDP );
		break;
	}

	ioctl( portFD,TIOCSETP,&ttyParms );

	return self;
}

//========================================================================
- (int) parity;
{
	// The EVENP bit has precedence whether or not the ODDP is set
	if( ttyParms.sg_flags & EVENP )
		return MISC_SP_EVEN;

	if( ttyParms.sg_flags & ODDP )
		return MISC_SP_ODD;
	else // <=> none of them are set
		return MISC_SP_NONE;
}

//========================================================================
- setMode: ( int )newMode
{	
	switch( newMode )
	{
	case MISC_SP_COOKED:
		ttyParms.sg_flags &= ~CBREAK;
		ttyParms.sg_flags &= ~RAW;
		break;

	case MISC_SP_CBREAK:
		ttyParms.sg_flags |= CBREAK;
		ttyParms.sg_flags &= ~RAW;
		break;

	case MISC_SP_RAW:
		ttyParms.sg_flags |= RAW;
		ttyParms.sg_flags &= ~CBREAK;
		break;
		
	default:
		ttyStruct.sg_flags &= ~CBREAK;
		ttyStruct.sg_flags &= ~RAW;
		break;
	}

	if( flags.connected )	// this flag ensures that the file descriptor is valid
		ioctl( portFD,TIOCSETP,&ttyParms );

	return self;
}

//========================================================================
- (int) mode;
{
	// I *think * that the RAW bit has precedence over the CBREAK
	if( ttyParms.sg_flags & RAW )
		return MISC_SP_RAW;

	if( ttyParms.sg_flags & CBREAK )
		return MISC_SP_CBREAK;
	else // <=> none of them are set
		return MISC_SP_COOKED;
}

//========================================================================
- translateNL: ( BOOL )yesNo
{
	if( yesNo )
		ttyParms.sg_flags |= CRMOD;
	else
		ttyParms.sg_flags &= ~CRMOD;

	if( flags.connected )	// this flag ensures that the file descriptor is valid
		ioctl( portFD,TIOCSETP,&ttyParms );

	status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;

	return self;
}

//========================================================================
- setEchoing: ( BOOL )yesNo
{
	if( yesNo )
		ttyParms.sg_flags |= ECHO;
	else
		ttyParms.sg_flags &= ~ECHO;

	if( flags.connected )	// this flag ensures that the file descriptor is valid
		ioctl( portFD,TIOCSETP,&ttyParms );

	status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;

	return self;
}

//========================================================================
- setDelegate: theConsumer
{
	// we only accpet the delegate if it responds to the reception notification message
	if( [theConsumer respondsTo:@selector( receiveChars:length: )] )
	{
		delegate = theConsumer;
		status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;
		return self;
	}
	delegate=nil;
	status = MISC_SP_OTHER; // this is just not to say we're OK if the delegate was refused.
	
	return nil;  // the return value is the thing to check in this case!
}

//========================================================================
- transmitChars: ( const char * )buffer length: ( int )length
{
	if( !flags.connected )
	{
		status = MISC_SP_NOT_OPEN;
		return self;
	}
	
	if( write( portFD,buffer,length ) < 0)
	{
		status = MISC_SP_SEE_ERRNO;
		return self;
	}

	status = MISC_SP_OK;
	return self;
}

//========================================================================
- ( void )checkReceiver
{
#define BUF_LEN	2048

	int		red;
	char	buf[BUF_LEN];

	red = read( portFD,buf, BUF_LEN );

	// If we got here something HAS arrived at the port I'm not sure if the check is useful
	// On the other hand is this comparison significantly long?
	if( red > 0 )
		[delegate receiveChars: buf length: red];
}

//========================================================================
void	*portEventHandler( int fdtag, id thePort )
{
	[thePort checkReceiver];
	return thePort;
}

//========================================================================
- suspendIO
{
	if( flags.connected )
	{
		if( !flags.suspended )
			DPSRemoveFD( portFD );
		flags.suspended = YES;
	}
	status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;
	return self;
}

//========================================================================
- continue { return [self resumeIO];}

//========================================================================
- resumeIO
{
	if( flags.connected && flags.suspended )
	{
		DPSAddFD( portFD,( DPSFDProc )portEventHandler, self, NX_MODALRESPTHRESHOLD );
		flags.suspended = NO;
	}
	status = ( flags.connected ) ? MISC_SP_OK : MISC_SP_NOT_OPEN;
	return self;
}

//========================================================================
- clearStatus;
{
	status = MISC_SP_OK;
	return self;
}

- ( BOOL ) isSuspended { return flags.suspended; }
- ( int ) fileDescriptor { return portFD; }
- ( int ) status; { return ( int ) status;}
- ( const char* ) statusString { return "OK!"; }
- ( BOOL ) isConnected { return flags.connected; }
- delegate { return delegate; }
- ( BOOL ) isEchoing { return ( ttyParms.sg_flags & ECHO ); }
- (BOOL) doesTranslateNL { return ( ttyParms.sg_flags & CRMOD ); }
- ( int ) baudRate { return 0; }
- ( const char* ) deviceName { return ( const char* ) device; }

@end

